﻿// ------------------------------------------------------------------------------
// <copyright file="DateControl.xaml.cs" company="skumaronline.com">
//    Copyright (C) 2012 Suresh Kumar T.
//
//    This program is free software; you can redistribute it and/or
//    modify it under the terms of the GNU General Public License
//    as published by the Free Software Foundation version 2
//    of the License.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    http://www.gnu.org/licenses/gpl-2.0.html
//
// </copyright>
// ------------------------------------------------------------------------------

namespace StoreAppLib.Controls
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Threading.Tasks;
    using StoreAppLib.Animations;
    using Windows.Foundation;
    using Windows.UI.Popups;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Controls.Primitives;
    using Windows.UI.Xaml.Input;
    using Windows.UI.Xaml.Media;

    /// <summary>
    /// Date control UI Class
    /// </summary>
    internal sealed partial class DateControl : UserControl, INotifyPropertyChanged
    {
        /// <summary>
        /// Popup control to host the date control
        /// </summary>
        private Popup popup;

        /// <summary>
        /// Holds the current date selected in the date control
        /// </summary>
        private DateTime currentDate;

        /// <summary>
        /// Holds the default date in the date picker control
        /// </summary>
        private DateTime deafaultDate;

        /// <summary>
        /// Hold the month data
        /// </summary>
        private ObservableCollection<MothItem> months;

        /// <summary>
        /// Hold the year data
        /// </summary>
        private ObservableCollection<YearItem> years;

        /// <summary>
        /// Hold the decades data
        /// </summary>
        private ObservableCollection<DecadeItem> decades;

        /// <summary>
        /// Date foreground color for days not in current month
        /// </summary>
        private SolidColorBrush dateOtherMonthForeground;

        /// <summary>
        /// The date picker control
        /// </summary>
        private DatePicker datePicker;        

        /// <summary>
        /// Initializes a new instance of the <see cref="DateControl" /> class
        /// </summary>
        /// <param name="datePicker">The date picker control</param>
        public DateControl(DatePicker datePicker)  
            : this()
        {
            if (datePicker.MinimumDate.Date > datePicker.MaximumDate.Date)
            {
                throw new ArgumentException(string.Format("Invalid application value defined.\nMinium Date [{0:dd-MMM-yyyy}] should be less than Maximum Date [{1:dd-MMM-yyyy}].", datePicker.MinimumDate, datePicker.MaximumDate));
            }

            this.datePicker = datePicker;

            this.DateForeground = datePicker.DateForeground;
            this.dateOtherMonthForeground = datePicker.DateOtherMonthForeground;

            if (datePicker.Date != DateTime.MinValue)
            {
                this.deafaultDate = datePicker.Date;
            }
            else
            {
                this.deafaultDate = DateTime.Now.Date;
            }

            this.Loaded += this.OnLoaded;

            this.popup = new Popup();

            this.popup.Closed += this.OnPopupClosed;

            this.popup.IsLightDismissEnabled = true;

            this.popup.Height = 360;
            this.Height = this.popup.Height;

            this.popup.Child = this;

            this.months = new ObservableCollection<MothItem>();
            this.years = new ObservableCollection<YearItem>();
            this.decades = new ObservableCollection<DecadeItem>();

            this.DataContext = this;

            this.CurrentDate = this.deafaultDate;
        }

        /// <summary>
        /// Prevents a default instance of the <see cref="DateControl" /> class from being created
        /// </summary>C
        private DateControl()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// Date changed event
        /// </summary>
        public event DateTimeChangedEventHandler DateTimeChanged;

        /// <summary>
        /// Property changed event
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Gets the date foreground color for days in current month
        /// </summary>
        public SolidColorBrush DateForeground { get; private set; }

        /// <summary>
        /// Gets date background color.
        /// </summary>
        public SolidColorBrush DateBackground 
        {
            get
            {
                return this.datePicker.DateBackground;
            }
        }

        /// <summary>
        /// Gets header foreground color for month, year and decade header
        /// </summary>
        public SolidColorBrush HeaderForeground
        {
            get
            {
                return this.datePicker.HeaderForeground;
            }
        }

        /// <summary>
        /// Gets month header foreground color
        /// </summary>
        public SolidColorBrush MonthHeaderForeground
        {
            get
            {
                return this.datePicker.MonthHeaderForeground;
            }
        }

        /// <summary>
        /// Gets month header background color
        /// </summary>
        public SolidColorBrush MonthHeaderBackground
        {
            get
            {
                return this.datePicker.MonthHeaderBackground;
            }
        }

        /// <summary>
        /// Gets default date and the selected date
        /// </summary>
        public DateTime CurrentDate
        {
            get
            {
                return this.currentDate;
            }

            private set
            {
                this.currentDate = value;

                this.CreateMonth();

                this.OnPropertyChanged();
            }
        }

        /// <summary>
        /// Gets month data
        /// </summary>
        public ObservableCollection<MothItem> Months
        {
            get
            {
                return this.months;
            }

            private set
            {
                this.months = value;

                this.OnPropertyChanged();
            }
        }

        /// <summary>
        /// Gets year data
        /// </summary>
        public ObservableCollection<YearItem> Years
        {
            get
            {
                return this.years;
            }

            private set
            {
                this.years = value;

                this.OnPropertyChanged();
            }
        }

        /// <summary>
        /// Gets the decade data
        /// </summary>
        public ObservableCollection<DecadeItem> Decades
        {
            get
            {
                return this.decades;
            }

            private set
            {
                this.decades = value;

                this.OnPropertyChanged();
            }
        }

        /// <summary>
        /// Show the date control
        /// </summary>
        public void Open()
        {
            this.popup.IsOpen = true;
        }

        /// <summary>
        /// Notifies listeners that a property value has changed.
        /// </summary>
        /// <param name="propertyName">Name of the property used to notify listeners.  This
        /// value is optional and can be provided automatically when invoked from compilers
        /// that support <see cref="CallerMemberNameAttribute"/>.</param>
        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var eventHandler = this.PropertyChanged;
            if (eventHandler != null)
            {
                eventHandler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Fires when this control is created and added to object tree.
        /// Attach event and set control layout
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Window.Current.Activated += this.OnCurrentWindowActivated;

            this.popup.Width = Window.Current.Bounds.Width;
            this.Width = this.popup.Width;
            this.popup.SetValue(Canvas.LeftProperty, 0);
            this.popup.SetValue(Canvas.TopProperty, Window.Current.Bounds.Height - this.Height);

            UIElement root = ((Frame)Window.Current.Content).Content as UIElement;
            GeneralTransform transform = this.datePicker.TransformToVisual(root);
            Point datePickerLocation = transform.TransformPoint(new Point(0, 0));

            double y = Math.Min(this.popup.ActualHeight, datePickerLocation.Y + this.datePicker.ActualHeight - Window.Current.Bounds.Height + this.popup.ActualHeight + 20);

            if (y > 0)
            {
                AnimationHelper.CompositeTransformTranslateY(root, -1 * y, 100);
            }
        }

        /// <summary>
        /// Fires when the date picker popup window is closed.
        /// Clean up events attached.
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private void OnPopupClosed(object sender, object e)
        {
            this.popup.Child = null;
            Window.Current.Activated -= this.OnCurrentWindowActivated;
            //// Modified as per jvinis suggestion => Issue ID 1368 
            //// AnimationHelper.CompositeTransformTranslateY(((Frame)Windows.UI.Xaml.Window.Current.Content).Content as Page, 0, 100);
            AnimationHelper.CompositeTransformTranslateY(((ContentControl)Windows.UI.Xaml.Window.Current.Content).Content as UIElement, 0, 100);
        }

        /// <summary>
        /// Fires when the current window successfully activated
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private void OnCurrentWindowActivated(object sender, Windows.UI.Core.WindowActivatedEventArgs e)
        {
            if (e.WindowActivationState == Windows.UI.Core.CoreWindowActivationState.Deactivated)
            {
                this.popup.IsOpen = false;
            }
        }

        /// <summary>
        /// Create month data
        /// </summary>
        private async void CreateMonth()
        {
            DateTime currentDate = this.deafaultDate;

            DateTime date = new DateTime(currentDate.Year, currentDate.Month, 1);

            ObservableCollection<MothItem> months = new ObservableCollection<MothItem>();

            months.Add(await this.CreateMonthAsynch(date));

            this.Months = months;
        }

        /// <summary>
        /// Create month data asynchronously
        /// </summary>
        /// <param name="monthDate">A date in the month</param>
        /// <returns>Returns a moth data asynchronously</returns>
        private Task<MothItem> CreateMonthAsynch(DateTime monthDate)
        {
            return Task.Run<MothItem>(() =>
            {
                List<Day> days = new List<Day>();

                DateTime date = new DateTime(monthDate.Year, monthDate.Month, 1);
                DateTime nextMonth = date.AddMonths(1);

                while (date.DayOfWeek != DayOfWeek.Sunday)
                {
                    date = date.AddDays(-1);
                }

                while (date < nextMonth)
                {
                    days.Add(new Day(date, monthDate, this.datePicker));

                    date = date.AddDays(1);
                }

                while (date.DayOfWeek != DayOfWeek.Sunday)
                {
                    days.Add(new Day(date, monthDate, this.datePicker));

                    date = date.AddDays(1);
                }

                return new MothItem(days.OrderBy(d => d.Date.Year).ThenBy(d => d.Date.Month).ThenBy(d => d.Date.Day));
            });
        }

        /// <summary>
        /// Create year data asynchronously
        /// </summary>
        /// <param name="yearDate">A date in the year</param>
        /// <returns>Returns an year data asynchronously</returns>
        private Task<YearItem> CreateYearAsynch(DateTime yearDate)
        {
            return Task.Run<YearItem>(() =>
            {
                List<DateTime> years = new List<DateTime>();

                for (int i = 1; i <= 12; i++)
                {
                    years.Add(new DateTime(yearDate.Year, i, 15));
                }

                return new YearItem(years);
            });
        }

        /// <summary>
        /// Create decades data asynchronously
        /// </summary>
        /// <param name="decadeDate">A date in the decade</param>
        /// <returns>Returns 20 years asynchronously</returns>
        private Task<DecadeItem> CreateDecadeAsynch(DateTime decadeDate)
        {
            return Task.Run<DecadeItem>(() =>
            {
                int year = decadeDate.Year;

                if (year % 10 != 0)
                {
                    year--;
                }

                while (year % 10 != 0)
                {
                    year--;
                }

                List<DateTime> decades = new List<DateTime>();

                for (int i = year + 1; i <= year + 20; i++)
                {
                    decades.Add(new DateTime(i, 1, 15));
                }

                return new DecadeItem(decades);
            });
        }

        /// <summary>
        /// When month is swiped, load next set of month and remove from the other side
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnMonthChanged(object sender, SelectionChangedEventArgs e)
        {
            FlipView flipView = sender as FlipView;

            if (flipView.SelectedIndex > -1)
            {
                int position = flipView.SelectedIndex + 1;
                int current;
                
                current = this.Months.Count;

                while (current > position + 2)
                {
                    this.Months.Remove(this.Months[this.Months.Count - 1]);
                    current--;
                }

                while (current < position + 2)
                {
                    this.Months.Add(await this.CreateMonthAsynch(this.Months[this.Months.Count - 1].Key.AddMonths(1)));
                    current++;
                }

                current = 1;

                while (current < position - 2)
                {
                    //this.Months.Remove(this.Months[0]);
                    current++;
                }

                while (current > position - 2)
                {
                    this.Months.Insert(0, await this.CreateMonthAsynch(this.Months[0].Key.AddMonths(-1)));
                    current--;
                }
            }
        }

        /// <summary>
        /// When year is swiped, load next set of year and remove from the other side
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnYearChanged(object sender, SelectionChangedEventArgs e)
        {
            FlipView flipView = sender as FlipView;

            if (flipView.SelectedIndex > -1)
            {
                int position = flipView.SelectedIndex + 1;
                int current;

                current = this.Years.Count;

                while (current > position + 2)
                {
                    this.Years.Remove(this.Years[this.Years.Count - 1]);
                    current--;
                }

                while (current < position + 2)
                {
                    this.Years.Add(await this.CreateYearAsynch(this.Years[this.Years.Count - 1].Key.AddYears(1)));
                    current++;
                }

                current = 1;

                while (current < position - 2)
                {
                    this.Years.Remove(this.Years[0]);
                    current++;
                }

                while (current > position - 2)
                {
                    this.Years.Insert(0, await this.CreateYearAsynch(this.Years[0].Key.AddYears(-1)));
                    current--;
                }
            }
        }

        /// <summary>
        /// When decade is swiped, load next set of decade and remove from the other side
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnDecadeChanged(object sender, SelectionChangedEventArgs e)
        {
            FlipView flipView = sender as FlipView;

            if (flipView.SelectedIndex > -1)
            {
                int position = flipView.SelectedIndex + 1;
                int current;

                current = this.Decades.Count;

                while (current > position + 2)
                {
                    this.Decades.Remove(this.Decades[this.Decades.Count - 1]);
                    current--;
                }

                while (current < position + 2)
                {
                    this.Decades.Add(await this.CreateDecadeAsynch(this.Decades[this.Decades.Count - 1][0].AddYears(20)));
                    current++;
                }

                current = 1;

                while (current < position - 2)
                {
                    this.Decades.Remove(this.Decades[0]);
                    current++;
                }

                while (current > position - 2)
                {
                    this.Decades.Insert(0, await this.CreateDecadeAsynch(this.Decades[0][0].AddYears(-20)));
                    current--;
                }
            }
        }

        /// <summary>
        /// When zoom out to year view, create year based on the month in zoom in view
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnLevel1ZoomStarted(object sender, SemanticZoomViewChangedEventArgs e)
        {
            if (e.IsSourceZoomedInView)
            {
                ObservableCollection<YearItem> years = new ObservableCollection<YearItem>();

                years.Add(await this.CreateYearAsynch(this.Months[1].Key));

                this.Years = years;
            }
        }

        /// <summary>
        /// When zoom out to decade view, create decade based on the year in zoom in view
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnLevel2ZoomStarted(object sender, SemanticZoomViewChangedEventArgs e)
        {
            if (e.IsSourceZoomedInView)
            {
                ObservableCollection<DecadeItem> decades = new ObservableCollection<DecadeItem>();

                decades.Add(await this.CreateDecadeAsynch(this.Years[1].Key));

                this.Decades = decades;
            }
        }

        /// <summary>
        /// Notify DatePicker when the day is selected and close
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnDaySelected(object sender, ItemClickEventArgs e)
        {
            this.currentDate = (e.ClickedItem as Day).Date;

            DateTimeEventArgs args = new DateTimeEventArgs(this.currentDate);

            args.Cancel = this.currentDate.Date < this.datePicker.MinimumDate.Date || this.currentDate > this.datePicker.MaximumDate.Date;

            if (this.DateTimeChanged != null)
            {
                this.DateTimeChanged(this.datePicker, args);
            }

            if (args.Cancel)
            {
                if (this.datePicker.ShowValidationMessage)
                {
                    string validationMessage = string.Empty;

                    if (this.currentDate.Date < this.datePicker.MinimumDate.Date)
                    {
                        validationMessage = this.datePicker.MinDateValidationMessage;
                    }
                    else
                    {
                        validationMessage = this.datePicker.MaxDateValidationMessage;
                    }

                    MessageDialog errorMessage = new MessageDialog(string.Format(validationMessage, this.currentDate, this.datePicker.MinimumDate.Date, this.datePicker.MaximumDate.Date));
                    errorMessage.DefaultCommandIndex = 1;
                    await errorMessage.ShowAsync();
                }

                return;
            }

            this.popup.IsOpen = false;            
        }

        /// <summary>
        /// When month is selected, create the month and zoom in to the month view
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnMonthSelected(object sender, ItemClickEventArgs e)
        {
            DateTime month = (DateTime)e.ClickedItem;

            ObservableCollection<MothItem> months = new ObservableCollection<MothItem>();

            months.Add(await this.CreateMonthAsynch(month));

            this.Months = months;

            level1Zoom.ToggleActiveView();
        }

        /// <summary>
        /// When year is selected, create the year and zoom in to the year view
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private async void OnYearSelected(object sender, ItemClickEventArgs e)
        {
            DateTime year = (DateTime)e.ClickedItem;

            ObservableCollection<YearItem> years = new ObservableCollection<YearItem>();

            years.Add(await this.CreateYearAsynch(year));

            this.Years = years;

            level2Zoom.ToggleActiveView();
        }

        /// <summary>
        /// Zoom out to year view when month heading is tapped
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private void OnMonthHeadingTapped(object sender, TappedRoutedEventArgs e)
        {
            level1Zoom.ToggleActiveView();
        }

        /// <summary>
        /// Zoom out to decade view when year heading is tapped
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private void OnYearHeadingTapped(object sender, TappedRoutedEventArgs e)
        {
            level2Zoom.ToggleActiveView();
        }

        /// <summary>
        /// Close application on close button tap
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">The argument</param>
        private void OnCloseTapped(object sender, TappedRoutedEventArgs e)
        {
            this.popup.IsOpen = false;
        }

        /// <summary>
        /// Class to represent a day
        /// </summary>
        public class Day
        {
            /// <summary>
            /// Holds a date in the current month 
            /// </summary>
            private DateTime currentMoth;

            /// <summary>
            /// Variable to hold the date picker reference.
            /// </summary>
            private DatePicker datepicker;            

            /// <summary>
            /// Initializes a new instance of the <see cref="Day" /> class
            /// </summary>
            /// <param name="day">The date in the current month or the date in the previous or next month to complete week</param>
            /// <param name="currentMoth">A date in the current month</param>
            /// <param name="datepicker">The date picker reference</param>
            public Day(
                DateTime day, 
                DateTime currentMoth, 
                DatePicker datepicker)
            {
                this.Date = day;
                this.currentMoth = currentMoth;
                this.datepicker = datepicker;
            }

            /// <summary>
            /// Gets the date
            /// </summary>
            public DateTime Date { get; private set; }

            /// <summary>
            /// Gets the background color for the date
            /// </summary>
            public Brush Background
            {
                get
                {
                    if (this.Date.Date.Equals(this.datepicker.Date.Date))
                    {
                        return this.datepicker.SelectedDateBackground;
                    }
                    else if (this.Date.Date < this.datepicker.MinimumDate.Date || this.Date.Date > this.datepicker.MaximumDate.Date)
                    {
                        return this.datepicker.DateDisabledBackground;
                    }
                    else
                    {
                        return this.datepicker.DateBackground;
                    }
                }
            }

            /// <summary>
            /// Gets the foreground color for the date
            /// </summary>
            public Brush Foreground
            {
                get
                {
                    if (this.Date.Date.Equals(DateTime.Now.Date))
                    {
                        return this.datepicker.DateTodayForeground;
                    }
                    else if (this.Date.Date < this.datepicker.MinimumDate.Date || this.Date.Date > this.datepicker.MaximumDate.Date)
                    {
                        return this.datepicker.DateDisabledForeground;
                    }
                    else if (this.Date.Month == this.currentMoth.Month)
                    {
                        return this.datepicker.DateForeground;
                    }
                    else
                    {
                        return this.datepicker.DateOtherMonthForeground;
                    }
                }
            }
        }

        /// <summary>
        /// Class to represent a month
        /// </summary>
        public class MothItem : ObservableCollection<Day>, IGrouping<DateTime, Day>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="MothItem" /> class
            /// </summary>
            /// <param name="dates">Dates in the month</param>
            public MothItem(IEnumerable<Day> dates)
                : base(dates)
            {
            }

            /// <summary>
            /// Gets 15 of this month as key
            /// </summary>
            public DateTime Key
            {
                get
                {
                    return this.Where(day => day.Date.Day == 15).FirstOrDefault().Date;
                }
            }
        }

        /// <summary>
        /// Class to represent an year
        /// </summary>
        public class YearItem : ObservableCollection<DateTime>, IGrouping<DateTime, DateTime>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="YearItem" /> class
            /// </summary>
            /// <param name="years">15 of every month in an year</param>
            public YearItem(IEnumerable<DateTime> years)
                : base(years)
            {
            }

            /// <summary>
            /// Gets first date of the year as key
            /// </summary>
            public DateTime Key
            {
                get { return this[0]; }
            }
        }

        /// <summary>
        /// Represent two decades
        /// </summary>
        public class DecadeItem : ObservableCollection<DateTime>, IGrouping<string, DateTime>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="DecadeItem" /> class
            /// </summary>
            /// <param name="years">15 january of every year</param>
            public DecadeItem(IEnumerable<DateTime> years)
                : base(years)
            {
            }

            /// <summary>
            /// Gets the first year and last year as key
            /// </summary>
            public string Key
            {
                get { return string.Format("{0:yyyy} - {1:yyyy}", this[0], this[this.Count - 1]); }
            }
        }
    }
}
